Skip to content

ui(desktop): design-system tightening — StatusDot + EmptyState primitives + token cleanup#285

Merged
jotyy merged 3 commits intomainfrom
refactor/desktop-component-uiux
May 7, 2026
Merged

ui(desktop): design-system tightening — StatusDot + EmptyState primitives + token cleanup#285
jotyy merged 3 commits intomainfrom
refactor/desktop-component-uiux

Conversation

@jotyy
Copy link
Copy Markdown
Member

@jotyy jotyy commented May 7, 2026

Summary

Two cohesive rounds of design-system work — both pure surface-level (no behavior or layout changes), both unblock the larger pane-level refactors that come next. Now spans 2 commits (#285's original + a follow-up), reviewable independently.


Round 1 — StatusDot primitive + design-token cleanup (commit 1)

StatusDot primitive

`desktop/src/components/ui/status-dot.tsx` — small colored dot indicating a state. Replaces ~14 hand-rolled `<span className="size-X rounded-full bg-X" />` instances.

cva-based variants matching real usage:

  • `variant`: success | destructive | warning | info | primary | muted | neutral
  • `size`: sm (6px, default) | md (8px) | lg (10px)
  • `withRing`: card-colored ring for badge dots that sit on top of icons
  • `pulse`: enables animate-pulse for "in progress" / "starting" states

Sites swapped: AppSurfacePane, IntegrationsList, ConnectIntegrationsStep, PublishScreen, SpaceBrowserDisplayPane, SpaceApplicationsExplorerPane, SpaceBrowserExplorerPane (5 sites), OperationsDrawer, ChatPane, RuntimeStatusIndicator, AuthPanel, CreatingView. RuntimeStatusIndicator's visual spec refactored from a Tailwind class string (`dotClass: "animate-pulse bg-warning"`) to typed enum fields (`dotVariant` + `dotPulse`).

Token cleanup

  • `border-border/35|40|55|60|70` → `border-border` (28 occurrences across 13 files)
  • `bg-foreground/6` → `bg-fg-6`; `bg-foreground/5` → `bg-fg-5`
  • The dropdown-menu / select shadcn templates keep their `bg-foreground/10` (they live inside `**:data-[state=...]` variant selectors that need a literal token).

Magic radii mapped to the standard scale: `[7px]`→md, `[10px]`→lg, `[12px]`→xl, `[14-22px]`→2xl, `[24-28px]`→3xl, `[4px]`→sm, `[9999px]`→full. The 18/20/28 → 2xl/3xl mappings shrink the affected dialog/sheet shells by 2-4px on a single visual element. Two intentional pixel-level decorations stay hand-tuned: tooltip arrow (`[2px]`) and the chat streaming-cursor block (`[1px]`).


Round 2 — EmptyState promoted to `ui/` + migrated (commit 2)

The dashboard's `EmptyState` was a great primitive locked in the wrong folder. Five other surfaces hand-rolled the same pattern (sidebar empties, automations placeholders, publish-wizard docs empties, marketplace gallery empties).

New API

```tsx
<EmptyState
icon={Icon} // optional LucideIcon
title="No items" // primary line (was `message`)
description="Sub line" // optional secondary (was `hint`)
action={<Button…/>} // optional CTA below
size="sm" | "md" // sm = compact dashboard look (default),
// md = chip-icon + text-sm + roomier padding
minHeight={number} // forces height (chart panels)
className="" // outer wrapper extras
/>
```

`sm` keeps the original dashboard visual. `md` wraps the icon in a muted chip and uses text-sm/text-xs for title/description — the look the sidebar empty states were rolling by hand.

Migrations

  • dashboard/{Board,Table,List,Chart} (4 sites): import path moved, `message` → `title`, `hint` → `description`. Visual unchanged.
  • AutomationsPane: deleted local 21-line `EmptyState` + 30-line `EmptyScheduled` re-implementations. The CTA "Ask the agent" now uses the new `action` slot.
  • SpaceApplicationsExplorerPane + SpaceBrowserExplorerPane: inline chip-icon empty states swapped for `<EmptyState size="md" />`.
  • publish/LivePreviewPanel: deleted local 8-line `DocsEmptyState`, swapped its 2 call sites.
  • marketplace/AppsGallery: "No apps available." and "No apps match the current filter." inline divs become real EmptyStates. Filter empty surfaces a Search icon + the existing Clear-filter CTA via the new `action` slot.
  • marketplace/MarketplaceGallery: Search icon for filter empty, LayoutGrid for the "no templates yet" case.

Deliberately not migrated

  • OperationsDrawer.EmptyNotice: 5 call sites use it for spinner + error states. Different semantic.
  • InternalSurfacePane.EmptyState: framed bordered card with `tone="error"` variant for file-preview failures. Different — it's a "preview frame".
  • BackgroundTasksPane + SubagentSessionsPane: dashed-border containers with single-line text. Intentionally different (placeholder zone).

Verification

  • `npm run desktop:typecheck` — clean (both commits)
  • `npm --prefix desktop run build:renderer` — clean (both commits)
  • `node --test` on AppShell + SpaceApplicationsExplorerPane — same pass count as upstream/main baseline (49 pass / 1 pre-existing fail on an unrelated `updateNotification` brittle assertion)
  • Manual: light + dark theme — sanity-check status pips on app surface header, sidebar app rows, integrations list, runtime status badge, ChatPane inbox unread badge, operations drawer alert badge
  • Manual: confirm dialog/sheet radii at 24/28→3xl don't visibly shrink (browser profile import dialog, workspace apps dialog, operations drawer popups)
  • Manual: empty states render correctly in: empty dashboard, empty workspace sidebar (apps + browsers), AutomationsPane "No schedules" with the agent CTA, marketplace galleries with no results

🤖 Generated with Claude Code

Two cohesive cleanups in one pass; both are pure surface-level (no
behavior or layout changes), and both unblock the larger pane-level
refactors that come next.

## New primitive: StatusDot

`desktop/src/components/ui/status-dot.tsx` — small colored dot
indicating a state (running, error, working, etc.). Replaces ~14
hand-rolled `<span className="size-X rounded-full bg-X" />` instances
across the shell so a single change here propagates everywhere.

cva-based variants matching real usage:
- `variant`: success | destructive | warning | info | primary | muted | neutral
- `size`: sm (6px, default) | md (8px) | lg (10px)
- `withRing`: card-colored ring for badge dots that sit on top of icons
- `pulse`: enables animate-pulse for "in progress" / "starting" states

Sites swapped: AppSurfacePane, IntegrationsList, ConnectIntegrationsStep,
PublishScreen, SpaceBrowserDisplayPane, SpaceApplicationsExplorerPane,
SpaceBrowserExplorerPane (5 sites), OperationsDrawer (badge dot,
withRing + lg), ChatPane (inbox-unread badge), RuntimeStatusIndicator
(refactored visual spec to carry `dotVariant` + `dotPulse` instead of
a Tailwind class string), AuthPanel, CreatingView.

## Token cleanup

Eliminate hand-rolled opacity hacks on canonical tokens — these were
masking earlier insufficient-contrast palettes that the current design
system has since resolved at the token level.

- `border-border/35|40|55|60|70` → `border-border` (28 occurrences across 13 files)
- `bg-foreground/6` → `bg-fg-6`; `bg-foreground/5` → `bg-fg-5`
- The dropdown-menu / select shadcn templates keep their
  `bg-foreground/10` because those live inside `**:data-[state=...]`
  variant selectors that need a literal token.

Magic-number radii mapped to the standard scale:

| Hand-rolled | Mapped to |
|---|---|
| `rounded-[7px]`  | `rounded-md` |
| `rounded-[10px]` | `rounded-lg` |
| `rounded-[12px]` | `rounded-xl` |
| `rounded-[14px]` | `rounded-2xl` |
| `rounded-[16px]` | `rounded-2xl` |
| `rounded-[18px]` | `rounded-2xl` |
| `rounded-[20px]` | `rounded-2xl` |
| `rounded-[22px]` | `rounded-2xl` |
| `rounded-[24px]` | `rounded-3xl` |
| `rounded-[28px]` | `rounded-3xl` |
| `rounded-[4px]`  | `rounded-sm` |
| `rounded-[9999px]` | `rounded-full` |

The 18/20/28 → 2xl/3xl mappings shrink the affected dialog/sheet
shells by 2-4px on a single visual element. Small enough to be
imperceptible side-by-side but consolidates the radius scale to
shadcn standards. Two intentional pixel-level decorations stay
hand-tuned: the tooltip arrow corner (rounded-[2px]) and the chat
streaming-cursor block (rounded-[1px]).

## Verification

- `npm run desktop:typecheck` — clean
- `npm --prefix desktop run build:renderer` — clean
- `node --test` on AppShell + SpaceApplicationsExplorerPane — same
  pass count as upstream/main baseline (49 pass / 1 pre-existing
  fail on an unrelated `updateNotification` brittle assertion).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jotyy jotyy requested a review from jeffreyliimerch as a code owner May 7, 2026 11:01
The dashboard's `EmptyState` was a great primitive locked in the wrong
folder. Five other surfaces hand-rolled the same pattern (sidebar
empties, automations placeholders, publish-wizard docs empties,
marketplace gallery empties), each with subtly different copy
hierarchy and chip treatments.

This PR moves `EmptyState` to `desktop/src/components/ui/`, extends
its API, and migrates the hand-rolled call sites.

## API

```tsx
<EmptyState
  icon={Icon}                 // optional LucideIcon
  title="No items"            // primary line (was `message`)
  description="Sub line"      // optional secondary (was `hint`)
  action={<Button…/>}         // optional CTA below
  size="sm" | "md"            // sm = compact dashboard look (default),
                              // md = chip-icon + text-sm + roomier padding
  minHeight={number}          // forces height (chart panels)
  className=""                // outer wrapper extras
/>
```

`sm` keeps the original dashboard visual (small unframed icon at
opacity-45, text-xs message). `md` wraps the icon in a muted chip and
uses text-sm/text-xs for title/description — the look that
SpaceApplicationsExplorerPane, SpaceBrowserExplorerPane, and
AutomationsPane were rolling by hand.

## Migrations

- **dashboard/{Board,Table,List,Chart}** (4 sites): import path moved,
  `message` → `title`, `hint` → `description`. Visual unchanged.
- **AutomationsPane**: deleted local 21-line `EmptyState` + 30-line
  `EmptyScheduled` re-implementations. The CTA "Ask the agent" now
  uses the new `action` slot.
- **SpaceApplicationsExplorerPane** + **SpaceBrowserExplorerPane**:
  inline chip-icon empty states swapped for `<EmptyState size="md" />`.
- **publish/LivePreviewPanel**: deleted local 8-line `DocsEmptyState`,
  swapped its 2 call sites.
- **marketplace/AppsGallery**: the "No apps available." and "No apps
  match the current filter." inline divs become real EmptyStates.
  Filter empty surfaces a Search icon + the existing Clear-filter CTA
  via the new `action` slot.
- **marketplace/MarketplaceGallery**: same treatment — Search icon for
  filter empty, LayoutGrid for the "no templates yet" case.

## Deliberately not migrated

- **OperationsDrawer.EmptyNotice**: 5 call sites use it for spinner +
  error states, not just empty. Different semantic; primitive doesn't
  cover it cleanly.
- **InternalSurfacePane.EmptyState**: framed bordered card with
  tone="error" variant for file-preview failures. Also different —
  it's a "preview frame", not a placeholder.
- **BackgroundTasksPane** + **SubagentSessionsPane**: dashed-border
  containers with single-line text. Intentionally different visual
  (placeholder zone, not centered empty state).

## Verification

- `npm run desktop:typecheck` — clean
- `npm --prefix desktop run build:renderer` — clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jotyy jotyy changed the title ui(desktop): extract StatusDot primitive + design-token cleanup pass ui(desktop): design-system tightening — StatusDot + EmptyState primitives + token cleanup May 7, 2026
jotyy added a commit that referenced this pull request May 7, 2026
Sweep through settings-scope panels (Account, Billing, Submissions)
and replace card-shaped `shadow-md` with `border border-border` so the
visual language matches `SettingsCard` and the full-screen settings
reference. Net effect: every settings card now reads as a quiet
hairline-bordered group, with the page itself doing the heavy visual
framing.

Sites updated:
- `auth/AuthPanel.tsx` — connected-providers list (3567), empty-state
  CTA card (3579), add-provider clickable row (3600), and the two
  `theme-shell` outer sections (4308, 4315) drop `shadow-card`.
- `billing/BillingSummaryCard.tsx` — section card (133).
- `settings/SubmissionsPanel.tsx` — destructive error card (399);
  swaps to `border border-destructive/24` to keep the warning tone.

Skipped (intentional):
- `auth/AuthPanel.tsx:3763, 4104` — Dialog popups with `shadow-xl`.
  Floating modals; lift is correct for them.
- Workspace surfaces outside settings (PaneCard, AppShell sections,
  dashboard panels, ChatPane, AppSurfacePane). Out of scope; their
  lift is part of the workspace-pane visual language.
- `IntegrationsPane.tsx` standalone path — when mounted in settings
  via `<IntegrationsPane embedded />` the embedded branch already
  bypasses the card-shell wrapper, so settings already render
  shadow-free for integrations. Standalone path stays as-is.

Tests:
- `auth/AuthPanel.test.mjs` brittle assertions updated:
  - `assert.match(source, /shadow-md/)` → `assert.doesNotMatch(...)`
  - empty-state CTA card pattern: `bg-card shadow-md` → `border
    border-border bg-card`
  - setup-loading section pattern: `rounded-[24px] ... shadow-card`
    → `rounded-3xl ...` (also picks up the rounded-3xl scale move
    from #285).
- `npm run desktop:typecheck` clean
- `node --test` 57 pass / 1 pre-existing baseline fail

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jotyy jotyy merged commit 624a674 into main May 7, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant